use std::{
    fs::{create_dir_all, remove_file, rename, File, OpenOptions},
    io::{Read, Write},
    path::Path,
    sync::Arc,
    time::SystemTime,
};

use crate::{
    cli::Cli,
    conf::{config_main::ConfigMain, metalink::Metalink, repo_conf::RepoConfig, repomd::Repomd},
    default::{
        CMDLINE_REPO_NAME, REPODATA_DIR_NAME, REPO_BASEURL_FILE_NAME, REPO_METADATA_FILE_NAME,
        REPO_METADATA_FILE_PATH, REPO_METADATA_MARKER, REPO_METALINK_FILE_NAME, RPM_CACHE_DIR_NAME,
        SOLVCACHE_DIR_NAME,
    },
    repo::Repo,
    utils::{
        check_root, download_single_file, get_file_md5, get_multi_progress, read_file_to_string,
        recursively_remove_dir,
    },
    Rdnf,
};
use anyhow::{bail, Result};
use console::{style, Term};
use indicatif::ProgressBar;
use reqwest::Client;
use tokio::fs::metadata;

impl Rdnf {
    pub async fn make_cache(
        repo_confs: Vec<RepoConfig>,
        conf: Arc<ConfigMain>,
        cli: Cli,
        term: Term,
    ) -> Result<Vec<Repo>> {
        let repos_iter = repo_confs
            .iter()
            .filter(|repo| repo.name.as_str() != CMDLINE_REPO_NAME && repo.base.enabled);
        let mut repos = Vec::new();
        if !cli.cacheonly {
            check_root()?;
            let size = repos_iter
                .filter(|r| r.base.lmetadata_expire > 0)
                .filter(|r| r.cache_name.is_some())
                .filter(|r| r.detail.meta_link.is_some() || r.detail.base_url.is_some())
                .count();
            let (_multi_pb, mut pbs) = get_multi_progress(size);
            let cli = Arc::new(cli);
            let mut handles = Vec::new();
            for repo in repo_confs {
                if repo.name.as_str() != CMDLINE_REPO_NAME
                    && repo.base.enabled
                    && repo.base.lmetadata_expire > 0
                    && repo.cache_name.is_some()
                    && (repo.detail.meta_link.is_some() || repo.detail.base_url.is_some())
                {
                    let pb = pbs.pop().unwrap();
                    let conf_clone = conf.clone();
                    let cli_clone = cli.clone();

                    handles.push((
                        repo.base.skip_if_unavailable,
                        repo.name.clone(),
                        tokio::spawn(init_repo(repo, conf_clone, cli_clone, pb)),
                    ));
                }
            }

            for (mut skip, repo_name, handle) in handles {
                if skip {
                    if let Ok(h) = handle.await {
                        if let Ok(repo) = h {
                            skip = false;
                            repos.push(repo)
                        }
                    }
                } else {
                    repos.push(handle.await??)
                }
                if cli.refresh {
                    let (_, width) = term.size();
                    let offset = (width - 10) as usize;
                    term.write_line(repo_name.as_str())?;
                    term.move_cursor_up(1)?;
                    term.move_cursor_right(offset)?;
                    let status = if skip {
                        format!("{}", style("Skip").red())
                    } else {
                        format!("{}", style("Done").green())
                    };
                    term.write_line(status.as_str())?;
                }
            }
        }
        Ok(repos)
    }
}
pub async fn init_repo(
    mut repo: RepoConfig,
    conf: Arc<ConfigMain>,
    cli: Arc<Cli>,
    pb: ProgressBar,
) -> Result<Repo> {
    let cache_dir = conf.cachedir.clone() + repo.cache_name.as_ref().unwrap() + "/";

    if should_sync_metadata(&conf, &repo).await? {
        recursively_remove_dir(cache_dir.clone() + REPODATA_DIR_NAME)?;
        recursively_remove_dir(cache_dir.clone() + SOLVCACHE_DIR_NAME)?;
    };
    let client = repo.get_req_client(&conf)?;
    let repomd = repo
        .get_repo_md(
            cache_dir.as_str(),
            cli.refresh,
            conf.keepcache,
            &client,
            &pb,
        )
        .await?;
    repo.get_repo(pb, repomd, client, cache_dir).await
}
impl RepoConfig {
    pub async fn get_repo_md(
        &mut self,
        cache_dir: &str,
        refresh: bool,
        keep_cache: bool,
        client: &Client,
        pb: &ProgressBar,
    ) -> Result<Repomd> {
        let repo_data_dir = cache_dir.to_owned() + REPODATA_DIR_NAME + "/";
        let meta_link_file = cache_dir.to_owned() + REPO_METALINK_FILE_NAME;

        let base_url_file = cache_dir.to_owned() + REPO_BASEURL_FILE_NAME;
        let repo_md_file = repo_data_dir.to_owned() + REPO_METADATA_FILE_NAME;
        let tmp_data_dir = cache_dir.to_owned() + "tmp/";
        let temp_repomd_file = tmp_data_dir.to_owned() + REPO_METADATA_FILE_NAME;
        let mut need_download = if self.detail.meta_link.is_some() {
            !Path::new(meta_link_file.as_str()).exists()
                || !Path::new(base_url_file.as_str()).exists()
        } else {
            !Path::new(repo_md_file.as_str()).exists()
        };
        let mut cookie: [u8; 16] = [0; 16];
        let mut replace_repo_md = false;
        let mut replace_base_url = false;
        if refresh {
            if self.detail.meta_link.is_some() {
                if Path::new(&meta_link_file).exists() {
                    cookie = get_file_md5(meta_link_file.as_str())?;
                }
            } else {
                if Path::new(&repo_md_file).exists() {
                    cookie = get_file_md5(repo_md_file.as_str())?;
                }
            }
            need_download = true;
        }

        if need_download {
            create_dir_all(tmp_data_dir.as_str())?;
            if let Some(meta_link_url) = &self.detail.meta_link {
                let tmp_metalink_file = tmp_data_dir.to_owned() + REPO_METALINK_FILE_NAME;
                download_single_file(client, &meta_link_url, &tmp_metalink_file).await?;
                let buf = read_file_to_string(tmp_metalink_file.as_str())?;
                if cookie != md5::compute(buf.as_bytes()).0 {
                    replace_repo_md = true;
                    let url = Metalink::download_repomd(
                        &buf,
                        client,
                        temp_repomd_file.as_str(),
                        &self.full_name,
                        pb,
                    )
                    .await?;
                    let tmp_base_url_file = tmp_data_dir.clone() + REPO_BASEURL_FILE_NAME;
                    let baseurl = url.trim_end_matches(REPO_METADATA_FILE_PATH);
                    self.detail.base_url = Some(baseurl.to_string());
                    if Path::new(&tmp_base_url_file).exists() {
                        remove_file(&tmp_base_url_file)?;
                    }
                    OpenOptions::new()
                        .write(true)
                        .create(true)
                        .open(tmp_base_url_file.as_str())?
                        .write_all(baseurl.as_bytes())?;
                    replace_base_url = true;
                    rename(tmp_base_url_file, base_url_file.as_str())?;
                    if Path::new(repo_md_file.as_str()).exists() {
                        if get_file_md5(repo_md_file.as_str())?
                            == get_file_md5(temp_repomd_file.as_str())?
                        {
                            replace_repo_md = false;
                        };
                    };
                };
                rename(tmp_metalink_file, meta_link_file)?;
            } else {
                if self.detail.base_url.is_none() {
                    bail!(
                        "Repo {} doesn't have base_url or meta_link ; please check",
                        self.full_name
                    );
                }
                let url =
                    self.detail.base_url.as_ref().unwrap().to_owned() + REPO_METADATA_FILE_PATH;
                download_single_file(client, url.as_str(), temp_repomd_file.as_str()).await?;
                replace_repo_md = true;
                if get_file_md5(temp_repomd_file.as_str())? == cookie {
                    replace_repo_md = false;
                };
            }
        }

        if self.detail.meta_link.is_some()
            && !replace_base_url
            && Path::new(base_url_file.as_str()).exists()
        {
            let mut buf = String::new();
            File::open(base_url_file.clone())?.read_to_string(&mut buf)?;
            self.detail.base_url = Some(buf.trim_end_matches("/").to_string() + "/");
        }
        let last_refresh_marker = cache_dir.to_owned() + REPO_METADATA_MARKER;

        if replace_repo_md {
            recursively_remove_dir(repo_data_dir.as_str())?;
            let solv_cache_dir = cache_dir.to_owned() + SOLVCACHE_DIR_NAME;
            recursively_remove_dir(solv_cache_dir)?;
            match remove_file(last_refresh_marker.clone()) {
                Ok(_) => {}
                Err(_) => {}
            };
            if !keep_cache {
                let rpms_cache_dir = cache_dir.to_owned() + RPM_CACHE_DIR_NAME + "/";
                recursively_remove_dir(rpms_cache_dir)?;
            }
            create_dir_all(repo_data_dir)?;
            rename(temp_repomd_file, repo_md_file.clone())?;
        }

        if need_download {
            OpenOptions::new()
                .write(true)
                .create(true)
                .open(last_refresh_marker)?;
        }

        let repomd = Repomd::parse_from(repo_md_file.as_str())?;
        Ok(repomd)
    }
}
async fn should_sync_metadata(conf: &ConfigMain, repo: &RepoConfig) -> Result<bool> {
    let refresh_flag =
        conf.cachedir.clone() + repo.cache_name.as_ref().unwrap() + "/" + REPO_METADATA_MARKER;
    let should = match metadata(refresh_flag).await {
        Ok(m) => {
            let mtime = m.modified()?;
            let now = SystemTime::now();
            let duration = now.duration_since(mtime)?;
            let s = duration.as_secs();
            if s as i128 >= repo.base.lmetadata_expire {
                true
            } else {
                false
            }
        }
        Err(_) => true,
    };
    Ok(should)
}

#[cfg(test)]
mod tests {
    // use futures_util::StreamExt;
    use reqwest::{header::CONTENT_LENGTH, ClientBuilder};
    // use tokio::io::AsyncWriteExt;

    #[tokio::test]
    async fn test_reqwest() {
        let package = vec![
            "389-ds-base-2.2.3-1.fc37.x86_64.rpm",
            "389-ds-base-devel-2.2.3-1.fc37.x86_64.rpm",
            "389-ds-base-libs-2.2.3-1.fc37.x86_64.rpm",
            "389-ds-base-snmp-2.2.3-1.fc37.x86_64.rpm",
        ];
        let prefix =
            "https://mirrors.aliyun.com/fedora/releases/37/Everything/x86_64/os/Packages/3/";
        let cli = ClientBuilder::new().user_agent("rdnf").build().unwrap();
        let primary_gz_url="https://mirrors.aliyun.com/fedora/releases/37/Everything/x86_64/os/repodata/54bbae6e9d4cd4865a55f7558daef86574cddc5f2a4f8a0d9c74f946e1a45dd3-primary.xml.gz";
        let _pkg_url = prefix.to_owned() + package[0];
        let p = cli.head(primary_gz_url).send().await.unwrap();
        let count = p
            .headers()
            .get(CONTENT_LENGTH)
            .unwrap()
            .to_str()
            .unwrap()
            .parse::<u64>()
            .unwrap();
        // let mut stream = cli.get(primary_gz_url).send().await.unwrap().bytes_stream();

        // let mut f = tokio::fs::OpenOptions::new()
        //     .append(true)
        //     .create(true)
        //     .open("tests/test.xml.gz")
        //     .await
        //     .unwrap();
        // let mut count = 0;
        // while let Some(items) = stream.next().await {
        //     let bytes = items.unwrap();
        //     count += bytes.len();
        //     f.write(bytes.as_ref()).await.unwrap();
        // }
        // dbg!(count);
        dbg!(count / 1024 / 1024);
    }
}
